/* Copyright (c) 2001 - 2013 OpenPlans - www.openplans.org. All rights reserved.
 * This code is licensed under the GPL 2.0 license, available at the root
 * application directory.
 */
package org.geoserver.feature.retype;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.geoserver.feature.RetypingFeatureCollection;
import org.geotools.data.DataStore;
import org.geotools.data.Query;
import org.geotools.data.FeatureListener;
import org.geotools.data.FeatureSource;
import org.geotools.data.Query;
import org.geotools.data.QueryCapabilities;
import org.geotools.data.ResourceInfo;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureLocking;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.data.simple.SimpleFeatureStore;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.Name;
import org.opengis.filter.Filter;

/**
 * Renaming wrapper for a {@link FeatureSource} instance, to be used along with
 * {@link RetypingDataStore}
 */
public class RetypingFeatureSource implements SimpleFeatureSource{

    SimpleFeatureSource wrapped;

    FeatureTypeMap typeMap;

    RetypingDataStore store;

    Map listeners = new HashMap();
    
    /**
     * Builds a retyping wrapper
     * @param wrapped
     * @param targetTypeName 
     * @param targetSchema The target schema can have a different name and less attributes than the original one
     * @return
     */
    public static SimpleFeatureSource getRetypingSource(SimpleFeatureSource wrapped, SimpleFeatureType targetSchema) throws IOException {
        FeatureTypeMap map = new FeatureTypeMap(wrapped.getSchema(), targetSchema);
        
        if(wrapped instanceof SimpleFeatureLocking) {
            return new RetypingFeatureLocking((SimpleFeatureLocking) wrapped, map);
        } else if(wrapped instanceof SimpleFeatureStore) {
            return new RetypingFeatureStore((SimpleFeatureStore) wrapped, map);
        } else {
            return new RetypingFeatureSource(wrapped, map);
        }
        
    }

    RetypingFeatureSource(RetypingDataStore ds,
            SimpleFeatureSource wrapped, FeatureTypeMap typeMap) {
        this.store = ds;
        this.wrapped = wrapped;
        this.typeMap = typeMap;
    }
    
    RetypingFeatureSource(SimpleFeatureSource wrapped, final FeatureTypeMap typeMap) throws IOException {
        this.wrapped = wrapped;
        this.typeMap = typeMap;
        this.store = new RetypingDataStore((DataStore) wrapped.getDataStore()) {
            @Override
            protected String transformFeatureTypeName(String originalName) {
                if(typeMap.getOriginalName().equals(originalName)) {
                    // rename
                    return typeMap.getName();
                } else if(typeMap.getName().equals(originalName)) {
                    // hide
                    return null;
                } else {
                    return originalName;
                }
            }
            
            @Override
            protected SimpleFeatureType transformFeatureType(SimpleFeatureType original)
                    throws IOException {
                if(typeMap.getOriginalName().equals(original)) {
                    return typeMap.featureType;
                } else {
                    return super.transformFeatureType(original);
                }
            }
            
            @Override
            public String[] getTypeNames() throws IOException {
                // Populate local hashmaps with new values.
                Map<String, FeatureTypeMap> forwardMapLocal = new ConcurrentHashMap<String, FeatureTypeMap>();
                Map<String, FeatureTypeMap> backwardsMapLocal = new ConcurrentHashMap<String, FeatureTypeMap>();
                
                forwardMapLocal.put(typeMap.getOriginalName(), typeMap);
                backwardsMapLocal.put(typeMap.getName(), typeMap);
                
                // Replace the member variables.
                forwardMap = forwardMapLocal;
                backwardsMap = backwardsMapLocal;
                
                return new String[] {typeMap.getName()};
            }
        };
    }
    
    /**
     * Returns the same name than the feature type (ie,
     * {@code getSchema().getName()} to honor the simple feature land common
     * practice of calling the same both the Features produces and their types
     * 
     * @since 1.7
     * @see FeatureSource#getName()
     */
    public Name getName() {
        return getSchema().getName();
    }

    public void addFeatureListener(FeatureListener listener) {
        FeatureListener wrapper = new WrappingFeatureListener(this, listener);
        listeners.put(listener, wrapper);
        wrapped.addFeatureListener(wrapper);
    }

    public void removeFeatureListener(FeatureListener listener) {
        FeatureListener wrapper = (FeatureListener) listeners.get(listener);
        if (wrapper != null) {
            wrapped.removeFeatureListener(wrapper);
            listeners.remove(listener);
        }
    }

    public ReferencedEnvelope getBounds() throws IOException {
        // not fully correct if we use this to shave attributes too, but this is
        // not in the scope now
        return wrapped.getBounds();
    }

    public ReferencedEnvelope getBounds(Query query) throws IOException {
        // not fully correct if we use this to shave attributes too, but this is
        // not in the scope now
        return wrapped.getBounds(store.retypeQuery(query, typeMap));
    }

    public int getCount(Query query) throws IOException {
        return wrapped.getCount(store.retypeQuery(query, typeMap));
    }

    public DataStore getDataStore() {
        return store;
    }

    public SimpleFeatureCollection getFeatures() throws IOException {
        return getFeatures(Query.ALL);
    }

    public SimpleFeatureCollection getFeatures(Query query) throws IOException {
        if (query.getTypeName() == null) {
            query = new Query(query);
            ((Query) query).setTypeName(typeMap.getName());
        } else if (!typeMap.getName().equals(query.getTypeName())) {
            throw new IOException("Cannot query this feature source with " + query.getTypeName()
                    + " since it serves only " + typeMap.getName());
        }
        
        //GEOS-3210, if the query specifies a subset of property names we need to take that into 
        // account
        SimpleFeatureType target = typeMap.getFeatureType();
        if ( query.getPropertyNames() != Query.ALL_NAMES ) {
            target = SimpleFeatureTypeBuilder.retype(target, query.getPropertyNames());
        }
        return new RetypingFeatureCollection(wrapped.getFeatures(store.retypeQuery(query, typeMap)),
                target);
    }

    public SimpleFeatureCollection getFeatures(Filter filter) throws IOException {
        return getFeatures(new Query(typeMap.getName(), filter));
    }

    public SimpleFeatureType getSchema() {
        return typeMap.getFeatureType();
    }

    public Set getSupportedHints() {
        return wrapped.getSupportedHints();
    }

    public ResourceInfo getInfo() {
        return wrapped.getInfo();
    }

    public QueryCapabilities getQueryCapabilities() {
        return wrapped.getQueryCapabilities();
    }

}
